import { type DetailedHTMLProps, forwardRef, useCallback, useEffect, useRef, type VideoHTMLAttributes } from "react";
import InfoModal from "../../components/Infomodal/Infomodal";
import { patchPlayPauseMethods } from "../../utils/patchPlayPauseMethods";

type VirtualVideoProps = DetailedHTMLProps<VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement> & {
  canPlayType?: (supported: boolean) => void;
  speed?: number;
};

const DEBUG_MODE = false;

// Just a mapping of file types to mime types, so we can check if the browser can play the file
// before having to fall back to using a fetch request.
const mimeTypeMapping = {
  // Supported
  mp4: "video/mp4",
  mp4v: "video/mp4",
  mpg4: "video/mp4",

  ogg: "video/ogg",
  ogv: "video/ogg",
  ogm: "video/ogg",
  ogx: "video/ogg",

  // Partially supported
  webm: "video/webm",

  // Unsupported
  avi: "video/avi",
  mov: "video/quicktime",
  qt: "video/quicktime",
};

const isBinary = (mimeType: string | null | undefined) => {
  if (!mimeType) {
    return false;
  }

  return mimeType.includes("octet-stream");
};

export const canPlayUrl = async (url: string) => {
  const video = document.createElement("video");

  const pathName = new URL(url, /^https?/.exec(url) ? undefined : window.location.href).pathname;

  const fileType = (pathName.split(".").pop() ?? "") as keyof typeof mimeTypeMapping;

  let fileMimeType: string | null | undefined = mimeTypeMapping[fileType];

  if (!fileMimeType) {
    const fileMeta = await fetch(url, {
      method: "GET",
      headers: {
        Range: "bytes=0-0",
      },
    });

    fileMimeType = fileMeta.headers.get("content-type");
  }

  // If the file is binary, we can't check if the browser can play it, so we just assume it can.
  const supported = isBinary(fileMimeType) || (!!fileMimeType && video.canPlayType(fileMimeType) !== "");
  const modalExists = document.querySelector(".ant-modal");

  if (!supported && !modalExists)
    InfoModal.error("There has been an error rendering your video, please check the format is supported");
  return supported;
};

export const VirtualVideo = forwardRef<HTMLVideoElement, VirtualVideoProps>((props, ref) => {
  const video = useRef<HTMLVideoElement | null>(null);
  const source = useRef<HTMLSourceElement | null>(null);
  const attachedEvents = useRef<[string, any][]>([]);

  const canPlayType = useCallback(
    async (url: string) => {
      let supported = false;

      if (url) {
        supported = await canPlayUrl(url);
      }

      if (props.canPlayType) {
        props.canPlayType(supported);
      }
      return supported;
    },
    [props.canPlayType],
  );

  const createVideoElement = useCallback(() => {
    const videoEl = document.createElement("video");

    videoEl.muted = !!props.muted;
    videoEl.controls = false;
    videoEl.preload = "auto";
    videoEl.playbackRate = props.speed ?? 1;

    videoEl.crossOrigin = "anonymous";

    Object.assign(videoEl.style, {
      top: "-9999px",
      width: 0,
      height: 0,
      position: "absolute",
    });

    if (DEBUG_MODE) {
      Object.assign(videoEl.style, {
        top: 0,
        zIndex: 10000,
        width: "200px",
        height: "200px",
        position: "absolute",
      });
    }

    video.current = videoEl;
  }, []);

  const attachRef = useCallback((video: HTMLVideoElement | null) => {
    if (video) video = patchPlayPauseMethods(video);
    if (ref instanceof Function) {
      ref(video);
    } else if (ref) {
      ref.current = video;
    }
  }, []);

  const attachEventListeners = () => {
    const eventHandlers = Object.entries(props)
      .filter(([key]) => key.startsWith("on"))
      .map(([evt, handler]) => [evt.toLowerCase(), handler]);

    const attached: [string, any][] = [];

    eventHandlers.forEach(([evt, handler]) => {
      const evtName = evt.replace(/^on/, "");

      video.current?.addEventListener(evtName, handler);
      attached.push([evtName, handler]);
    });

    attachedEvents.current = attached;
  };

  const detachEventListeners = () => {
    if (!video.current) return;

    (attachedEvents.current ?? []).forEach(([evt, handler]) => {
      video.current?.removeEventListener(evt, handler);
    });

    attachedEvents.current = [];
  };

  const unloadSource = () => {
    if (source && video) {
      video.current?.pause();
      source.current?.setAttribute("src", "");
      video.current?.load();
    }
  };

  const attachSource = useCallback(() => {
    if (!video.current) return;

    video.current?.pause();

    if (source.current) unloadSource();

    const sourceEl = document.createElement("source");

    sourceEl.setAttribute("src", props.src ?? "");
    video.current?.appendChild(sourceEl);

    source.current = sourceEl;
  }, [props.src]);

  useEffect(() => {
    detachEventListeners();
    attachEventListeners();
  });

  // Create a video tag
  useEffect(() => {
    createVideoElement();
    attachEventListeners();
    canPlayType(props.src ?? "").then((canPlay) => {
      if (canPlay && video.current) {
        attachSource();
        attachRef(video.current);

        document.body.append(video.current!);
      }
    });

    return () => {
      // Handle video cleanup
      detachEventListeners();
      unloadSource();
      attachRef(null);
      video.current?.remove();
      video.current = null;
    };
  }, []);

  useEffect(() => {
    if (video.current && props.muted !== undefined) {
      video.current.muted = props.muted;
    }
  }, [props.muted]);

  return null;
});
